/**
 * RetryOnExceptionStrategyAdvisor.java
 * 
 * Created on Jun 4, 2008 at 7:41:21 PM
 */
package org.lego4j.retryonexception;

import static java.lang.String.format;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

import org.lego4j.AbstractMethodWithAnnotationPointcutAdvisor;
import org.lego4j.IValidableAdvisor;
import org.lego4j.RetryOnException;
import org.lego4j.RetryOnExceptionPolicyInterceptor;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;

/**
 * Spring AOP retry on exception strategy aspect for at the class or method level
 *
 * <p>Retries the execution of a method a number of times if
 * a predefined exception is thrown by the method, based on a given RetryOnExceptionStrategy.
 * 
 * @author <a href="alexguev@gmail.com">Alexei Guevara</>
 * @version $Revision:$ updated on $Date:$ by $Author:$
 */
public class RetryOnExceptionPolicyAdvisor
    extends AbstractMethodWithAnnotationPointcutAdvisor
    implements IValidableAdvisor {

    private static final long serialVersionUID = 8010241312673979624L;

    static class MethodAndAnnotation {
        final Method method;
        final RetryOnException annotation;
        MethodAndAnnotation( Method  theMethod, RetryOnException theAnnotation ) {
            method = theMethod;
            annotation = theAnnotation;
        }
    }
    
    /**
     */
    public RetryOnExceptionPolicyAdvisor() {
        super( RetryOnException.class, new RetryOnExceptionPolicyInterceptor() );
    }
    
    /**
     * {@inheritDoc}
     */
    public void validate(Class<?> theBeanClass) throws BeanDefinitionValidationException {
        validateDeclarationOfMethods( theBeanClass );
    }

    /*
     * makes sure that each method annotated with @RetryOnException must:
     * 
     * - have exception specified in the annotation in the throws clause
     * - OR, exception specified in the annotation is a subclass of at least one class in the throws clause 
     * 
     */
    private void validateDeclarationOfMethods(Class<?> theTargetClass) {
        Set<MethodAndAnnotation> annotatedMethods = collectionAnnotatedMethods( theTargetClass );
        
        for ( MethodAndAnnotation m : annotatedMethods ) {
        	validateExListAnnotationIsNotEmpty( m );
            validateExInAnnotationOrSuperClassOfItIsInThrowsClause( m );
        }
        
    }

    private void validateExListAnnotationIsNotEmpty(MethodAndAnnotation m) {
    	if (m.annotation.e().length==0) {
            throw new BeanDefinitionValidationException("The e attribute must have at least one exception");
    	}
	}

	private void validateExInAnnotationOrSuperClassOfItIsInThrowsClause( MethodAndAnnotation theMethod ) {
        Class<? extends Exception> exInAnnotation = theMethod.annotation.e()[0];
        Class<?>[] exsInThrowsClause = theMethod.method.getExceptionTypes();

        for ( Class<?> exType : exsInThrowsClause ) {
            if (exType.isAssignableFrom( exInAnnotation )) {
                return;
            }
        }
        
        throw new BeanDefinitionValidationException( format( 
                "The throws clause must include one class that is equals or is a super class of %s", 
                exInAnnotation.getName() ) );
    }

    private Set<MethodAndAnnotation> collectionAnnotatedMethods(Class<?> theTargetClass) {
        Set<MethodAndAnnotation> annotatedMethods = new HashSet<MethodAndAnnotation>();
        
        for (Method m : theTargetClass.getMethods()) {
            RetryOnException annotation = m.getAnnotation( RetryOnException.class );
            if ( annotation!=null ) {
                annotatedMethods.add( new MethodAndAnnotation( m, annotation ) );
            }
        }
        
        return annotatedMethods;
    }
        
}
